Skip to content
Souloss
Go back

为什么 TCP 建立连接需要三次握手

深入解析 TCP 建立连接时三次握手的设计原理,为什么两次握手无法保证可靠性,四次握手是否必要。

为什么 TCP 建立连接需要三次握手

TCP(Transmission Control Protocol)是互联网中最核心的传输层协议之一,它提供了可靠的、面向连接的字节流服务。在 TCP 通信开始之前,通信双方需要通过「三次握手」建立连接。这个看似简单的过程,背后隐藏着精妙的设计考量。

一、TCP 连接的建立过程

1.1 三次握手的完整流程

sequenceDiagram

    participant C as 客户端

    participant S as 服务端

  

    C->>S: SYN seq=x

    S->>C: SYN ACK seq=y, ack=x+1

    C->>S: ACK seq=x+1, ack=y+1

  

    Note over C,S: 连接建立完成

三次握手的过程:

  1. 第一次握手(SYN):客户端发送一个 SYN 包(同步序列号),请求建立连接

  2. 第二次握手(SYN + ACK):服务端收到 SYN 后,返回 SYN+ACK 包,表示同意建立连接

  3. 第三次握手(ACK):客户端再发送一个 ACK 包,确认收到服务端的确认

1.2 抓包验证

使用 Wireshark 或 tcpdump 可以观察到实际的三次握手过程:


# 使用 tcpdump 抓取 TCP 握手包

tcpdump -i eth0 'tcp[tcpflags] & (tcp-syn|tcp-ack|tcp-fin) != 0'

  

# 观察到的三次握手

# 1. Client -> Server: SYN Seq=0

# 2. Server -> Client: SYN ACK Seq=0 Ack=1

# 3. Client -> Server: ACK Seq=1 Ack=1
bash

二、为什么不能两次握手?

这是理解 TCP 设计的核心问题。直觉上,似乎确认和应答机制用两次就够了,为什么要画蛇添足地多一次?

2.1 两次握手无法保证可靠性

让我们分析一个具体场景:

sequenceDiagram

    participant C as 客户端

    participant S as 服务端

  

    Note over C,S: 场景:旧数据包延迟到达

  

    C->>S: SYN seq=1000 (请求连接)

    S->>C: SYN ACK seq=2000, ack=1001 (同意连接)

  

    Note over C,S: 连接建立,发送数据

  

    S->>C: 数据 (来自旧的延迟 SYN 请求)

    C->>S: ??? (困惑:这个数据是谁发的?)

问题的本质:TCP 是全双工通信,两次握手只能让一端确认另一端的存在,但无法让双方互相确认对方都「能够正常收发」。

2.2 历史请求的困扰

考虑这个场景:

  1. 客户端发送了一个 SYN 请求(seq=100),这个包在网络中迷路了

  2. 客户端超时重传了新的 SYN 请求(seq=200)

  3. 旧的 SYN(seq=100)最终到达服务端

  4. 如果只有两次握手,服务端会认为这是一个有效的连接请求并建立连接

  5. 而客户端并不知道这个「幽灵连接」的存在

sequenceDiagram

    participant C as 客户端

    participant S as 服务端

  

    Note over C,S: 场景:SYN 延迟到达

  

    rect rgb(255, 230, 230)

    C->>S: SYN seq=100 (迷路)

    end

  

    C->>S: SYN seq=200 (重传)

    S->>C: SYN ACK seq=?, ack=201

  

    Note over C,S: 客户端认为连接已建立,发送数据

    S->>C: 数据

  

    rect rgb(230, 255, 230)

    Note over S: 旧 SYN 到达,服务端错误建立连接

    end

2.3 两次握手无法同步初始序列号

TCP 的可靠传输依赖于序列号(Sequence Number)。每个字节的数据都有一个序列号,用于:

三次握手中的第二次握手,服务端要将自己的初始序列号(Initial Sequence Number,ISN 告知客户端。第三次握手时,客户端也要将自己的 ISN 告知服务端。只有这样,双方才能正确地:

  1. 对发送的数据进行编号

  2. 对接收的数据进行确认

  3. 处理乱序到达的数据包

三、为什么不需要四次握手?

既然三次握手都各有其道理,为什么不是四次、五次?能否更少?

3.1 四次握手是否必要?

sequenceDiagram

    participant C as 客户端

    participant S as 服务端

  

    C->>S: SYN seq=x

    S->>C: ACK ack=x+1

    S->>S: (服务端处理)

    S->>C: SYN seq=y

    C->>S: ACK ack=y+1

  

    Note over C,S: 需要 4 次交互

四次握手在理论上是可行的,但效率低下。关键在于:服务端的 SYNACK 可以合并成一次传输。

3.2 合并的艺术

在 TCP 的状态转换中,服务端收到客户端的 SYN 后,可以立即发送 SYN+ACK(其中 ACK 是对客户端 SYN 的确认,SYN 是服务端自己的初始化序列号)。这两次信息可以同时发送,因此两次交互变成了三次。

flowchart LR

    subgraph 两次握手的问题

        A[客户端发送 SYN] --> B[服务端发送 ACK]

        B --> C[服务端发送 SYN]

        C --> D[客户端发送 ACK]

    end

  

    subgraph 三次握手的优化

        E[客户端发送 SYN] --> F[服务端发送 SYN+ACK]

        F --> G[客户端发送 ACK]

    end

核心洞察:服务端对客户端 SYN 的「确认」和 服务端自己的「SYN」在时间上紧密相关,合并发送是自然的优化。

四、三次握手的深层设计哲学

4.1 最小化连接建立时间

网络通信中,延迟是敌人。三次握手是最小化的、能够建立可靠全双工连接的交互次数:

flowchart TD

    A[需要什么?] --> B{能否单向?}

    B -->|否| C{需要同步双方序列号?}

    B -->|是| D[一次握手]

    C -->|否| E[两次握手]

    C -->|是| F{能否合并?}

    F -->|ACK+SYN| G[三次握手]

    F -->|不能合并| H[四次握手]

4.2 状态机的精妙设计

TCP 的连接管理通过状态机来实现:

flowchart LR

    subgraph 客户端状态

        C1[CLOSED] --> C2[SYN_SENT]

        C2 --> C3[ESTABLISHED]

    end

  

    subgraph 服务端状态

        S1[CLOSED] --> S2[LISTEN]

        S2 --> S3[SYN_RCVD]

        S3 --> S4[ESTABLISHED]

    end

  

    C1 --"主动打开"--> S2

    C2 --"收到 SYN"--> S3

    S3 --"收到 ACK"--> C3

三次握手完美对应了状态机的转换:

  1. 客户端发送 SYN → 客户端进入 SYN_SENT,服务端进入 SYN_RCVD

  2. 服务端发送 SYN+ACK → 双方都看到了对方的「我准备好了」

  3. 客户端发送 ACK → 双方都确认了对方知道自己准备好了

4.3 防御性的设计

TCP 的设计考虑到了网络的不可靠性:

| 设计要素              | 作用                   |

| --------------------- | ---------------------- |

| 序列号随机化          | 防止攻击者猜测序列号   |

| 超时重传              | 处理丢包               |

| 最大报文段寿命(MSS) | 防止迷途数据包永久存活 |

| 3-way handshake       | 确保双方都能收发       |

五、半关闭与四次挥手

既然提到了三次握手,就不得不提 TCP 连接的关闭——四次挥手(Four-way Wave)。

5.1 为什么关闭需要四次?

sequenceDiagram

    participant C as 客户端

    participant S as 服务端

  

    C->>S: FIN seq=u

    S->>C: ACK ack=u+1

  

    Note over S: 客户端已无数据发送
但服务端可能还有数据要发     S->>C: FIN seq=v     C->>S: ACK ack=v+1     Note over C,S: 连接关闭

关闭连接时,需要确保双方都没有数据要发送了。TCP 是全双工的,每个方向都必须单独关闭:

5.2 半关闭状态

TCP 允许连接的一端在接收完数据后单独关闭发送功能,这称为「半关闭」:

flowchart LR

    A[全双工] -->|close write| B[半双工]

    B -->|close read| C[完全关闭]

使用 shutdown() 函数可以实现半关闭,而 close() 会同时关闭读和写。

六、性能与安全的权衡

6.1 SYN Flood 攻击

三次握手设计的一个副作用是:服务端在收到客户端的 SYN 后,会分配资源并进入 SYN_RCVD 状态,等待客户端的 ACK。

攻击者发送大量 SYN 包但不完成握手,就会耗尽服务端的连接资源:


# SYN Flood 示意

for i in $(seq 1 100000); do

    curl -S http://target.com &

done
bash

防御手段包括:

6.2 快速打开(TCP Fast Open)

为了减少连接建立的开销,TCP Fast Open(TFO)允许在第一次握手时就交换数据:

sequenceDiagram

    participant C as 客户端

    participant S as 服务端

  

    C->>S: SYN + Cookie + HTTP请求

    S->>C: SYN ACK + 响应数据

  

    Note over C,S: 数据交换在第一次交互完成

但这需要在之前已经建立过连接并获得过 Cookie,安全性需要仔细考虑。

七、总结

TCP 三次握手的设计完美地平衡了多个目标:

| 目标   | 三次握手如何实现       |

| ------ | ---------------------- |

| 可靠性 | 通过序列号确认数据接收 |

| 全双工 | 双方都能发送和接收     |

| 效率   | 最小化交互次数         |

| 灵活性 | 支持半关闭等高级特性   |

| 安全性 | 序列号随机化防御攻击   |

理解三次握手的设计原理,不仅有助于网络编程,更是理解分布式系统可靠性的起点。在设计高并发系统时,这些原理依然有重要的指导意义。

参考引用



评论区

文明评论,共建和谐社区